﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using OpenTK;
using Tao.Cg;

namespace LasRendering
{
    public static class ShaderHelper
    {
        private static Dictionary<IntPtr, Dictionary<string, IntPtr>> globalProgramParamList = new Dictionary<IntPtr, Dictionary<string, IntPtr>>();

        public static int VertexProfile
        {
            get;
            private set;
        }

        public static int GeometryProfile
        {
            get;
            private set;
        }

        public static int FragmentProfile
        {
            get;
            private set;
        }

        public static IntPtr Context
        {
            get;
            private set;
        }

        private static void AssertCgNoError()
        {
#if DEBUG
            int error;
            string errorString = Cg.cgGetLastErrorString(out error);
            Debug.Assert(error == Cg.CG_NO_ERROR, errorString);
#endif
        }

        private static bool cgInitialized = false;
        public static void InitCg()
        {
            if (cgInitialized)
            {
                return;
            }
            cgInitialized = true;

#if DEBUG
            cgGLSetDebugMode(true);
#else
            cgGLSetDebugMode(false);
#endif

            VertexProfile = CgGl.cgGLGetLatestProfile(CgGl.CG_GL_VERTEX);
            CgGl.cgGLSetOptimalOptions(VertexProfile);

            GeometryProfile = CgGl.cgGLGetLatestProfile(CgGl.CG_GL_GEOMETRY);
            CgGl.cgGLSetOptimalOptions(GeometryProfile);

            FragmentProfile = CgGl.cgGLGetLatestProfile(CgGl.CG_GL_FRAGMENT);
            CgGl.cgGLSetOptimalOptions(FragmentProfile);

            Context = Cg.cgCreateContext();
            CgGl.cgGLSetManageTextureParameters(Context, true);
            AssertCgNoError();
        }

        public static void UninitCg()
        {
            Debug.Assert(globalProgramParamList.Count == 0);

            Cg.cgDestroyContext(Context);
            AssertCgNoError();

            Context = IntPtr.Zero;
            VertexProfile = 0;
            GeometryProfile = 0;
            FragmentProfile = 0;
        }

        public static IntPtr LoadProgram(int profile, string filename)
        {
            return LoadProgram(profile, filename, null);
        }

        public static IntPtr LoadProgram(int profile, string filename, string entry)
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("LasRendering.Shaders." + filename))
            {
                StreamReader sr = new StreamReader(stream);
                string source = sr.ReadToEnd();
                IntPtr program = Cg.cgCreateProgram(
                    Context,
                    Cg.CG_SOURCE,
                    source,
                    profile,
                    entry,
                    null);
                AssertCgNoError();

                CgGl.cgGLLoadProgram(program);
                AssertCgNoError();

                globalProgramParamList.Add(program, new Dictionary<string, IntPtr>());

                return program;
            }
        }

        public static void UnloadProgram(IntPtr program)
        {
            globalProgramParamList.Remove(program);
            Cg.cgDestroyProgram(program);
            AssertCgNoError();
        }

        private static IntPtr GetParameter(IntPtr program, string name)
        {
            IntPtr param;
            var paramList = globalProgramParamList[program];
            if (!paramList.TryGetValue(name, out param))
            {
                param = Cg.cgGetNamedParameter(program, name);
                AssertCgNoError();
                paramList.Add(name, param);
            }
            return param;
        }

        public static void SetParameter(IntPtr program, string name, params object[] value)
        {
            IntPtr param = GetParameter(program, name);

            Debug.Assert(value.Length > 0);
            Type type = value[0].GetType();

            switch (value.Length)
            {
                case 1:
                    if (type == typeof(int))
                    {
                        Cg.cgSetParameter1i(param, (int)value[0]);
                    }
                    else if (type == typeof(float))
                    {
                        Cg.cgSetParameter1f(param, (float)value[0]);
                    }
                    else if (type == typeof(double))
                    {
                        Cg.cgSetParameter1d(param, (double)value[0]);
                    }
                    else if (type == typeof(Vector3))
                    {
                        var v = (Vector3)value[0];
                        Cg.cgSetParameter3f(param, v.X, v.Y, v.Z);
                    }
                    else if (type == typeof(Vector4))
                    {
                        var v = (Vector4)value[0];
                        Cg.cgSetParameter4f(param, v.X, v.Y, v.Z, v.W);
                    }
                    else if (type == typeof(Matrix4))
                    {
                        SetMatrixParameter(program, name, (Matrix4)value[0]);
                    }
                    else
                    {
                        goto default;
                    }
                    break;

                case 2:
                    if (type == typeof(int))
                    {
                        Cg.cgSetParameter2i(param, (int)value[0], (int)value[1]);
                    }
                    else if (type == typeof(float))
                    {
                        Cg.cgSetParameter2f(param, (float)value[0], (float)value[1]);
                    }
                    else if (type == typeof(double))
                    {
                        Cg.cgSetParameter2d(param, (double)value[0], (double)value[1]);
                    }
                    else
                    {
                        goto default;
                    }
                    break;

                case 3:
                    if (type == typeof(int))
                    {
                        Cg.cgSetParameter3i(param, (int)value[0], (int)value[1], (int)value[2]);
                    }
                    else if (type == typeof(float))
                    {
                        Cg.cgSetParameter3f(param, (float)value[0], (float)value[1], (float)value[2]);
                    }
                    else if (type == typeof(double))
                    {
                        Cg.cgSetParameter3d(param, (double)value[0], (double)value[1], (double)value[2]);
                    }
                    else
                    {
                        goto default;
                    }
                    break;

                case 4:
                    if (type == typeof(int))
                    {
                        Cg.cgSetParameter4i(param, (int)value[0], (int)value[1], (int)value[2], (int)value[3]);
                    }
                    else if (type == typeof(float))
                    {
                        Cg.cgSetParameter4f(param, (float)value[0], (float)value[1], (float)value[2], (float)value[3]);
                    }
                    else if (type == typeof(double))
                    {
                        Cg.cgSetParameter4d(param, (double)value[0], (double)value[1], (double)value[2], (double)value[3]);
                    }
                    else
                    {
                        goto default;
                    }
                    break;

                default:
                    Debug.Fail("NotSupportedParameter");
                    break;
            }
            AssertCgNoError();
        }

        public static void SetMatrixParameter(IntPtr program, string name, int matrix, int transform)
        {
            IntPtr param = GetParameter(program, name);
            CgGl.cgGLSetStateMatrixParameter(param, matrix, transform);
            AssertCgNoError();
        }

        public static void SetMatrixParameter(IntPtr program, string name, Matrix4 matrix)
        {
            IntPtr param = GetParameter(program, name);
            var handle = GCHandle.Alloc(matrix, GCHandleType.Pinned);
            CgGl.cgGLSetMatrixParameterfc(param, handle.AddrOfPinnedObject());
            AssertCgNoError();
            handle.Free();
        }

        public static void SetTextureParameter(IntPtr program, string name, int texture)
        {
            IntPtr param = GetParameter(program, name);
            CgGl.cgGLSetTextureParameter(param, texture);
            //Automatic texture management is enable by cgGLSetManageTextureParameters, no need to enable/disable texture paramters.
            //CgGl.cgGLEnableTextureParameter(param);
            AssertCgNoError();
        }

        public static void BindProgram(int profile, IntPtr program)
        {
            CgGl.cgGLBindProgram(program);
            AssertCgNoError();

            CgGl.cgGLEnableProfile(profile);
            AssertCgNoError();
        }

        public static void UnbindProgram(int profile)
        {
            CgGl.cgGLUnbindProgram(profile);
            AssertCgNoError();

            CgGl.cgGLDisableProfile(profile);
            AssertCgNoError();
        }

        [SuppressUnmanagedCodeSecurity]
        [DllImport("cgGL.dll", CallingConvention = CallingConvention.Cdecl)]
        internal static extern void cgGLSetDebugMode([MarshalAs(UnmanagedType.Bool)] bool debug);
    }
}
